NutriScan
Photograph a nutrition label and it becomes tracked macros: AI vision reads the calories, protein, carbs, fat and fibre straight off the packet. No barcode database, no manual typing. Around that sits a full tracker: daily goals, a food library, one-tap meal templates, weekly trends, and an AI assistant that already knows your log.
The scan pipeline
Images are prepared in the browser before they ever leave the phone: a touch-friendly canvas cropper isolates the label, then the image is resized and grayscaled for smaller uploads, faster scans, and cheaper AI calls. On the server every upload passes one validating gate: the real format is read from the file's bytes (JPEG, PNG or WebP; the browser's label is ignored), dimensions are checked against a 30-megapixel cap before a pixel is decoded, and the image is re-encoded as a fresh JPEG with no metadata. Nothing else reaches storage or the model. Gemini 2.5 Flash reads the label, with automatic fallback to 3.6 Flash if the primary model fails, and its answer is checked against an explicit schema (allowed fields, sane numeric bounds; text printed on the photo is data, never instructions) and normalized into clean macros (kcal · protein · carbs · fat · fibre) before anything is shown or logged.
Auth without passwords
- ✗No passwords, ever: sign in with Google OAuth or a 6-digit emailed code. Nothing to store, leak, or reset
- ✓Verified, not trusted: every JWT decode goes through one verifying helper (shared-secret HS256 and cached JWKS) - pinned-ruleset semgrep reports zero findings, and the CI that guards it pins every action and scanner to an immutable hash
- ✓Isolated at the storage layer: each request binds the user to the database session, and row-level security policies do the rest. App bugs can't read another user's rows
- ✓Contained when hijacked: a delete spree freezes the account on two layers (in-process lock, Supabase admin ban) and every deleted row is already in a 30-day recycle bin
- ✓Deletable, carefully: full account deletion is a first-class Settings action, gated on a sign-in within the last five minutes (the JWT
amrclaim, not the refreshableiat)
F-01Row-level security everywhere
All 11 tables carry per-user RLS policies, so isolation is enforced by Postgres itself, not by remembering a WHERE clause in every query.
F-02Vision with a fallback
Gemini 2.5 Flash is the primary label reader; if it errors, the scan retries on 3.6 Flash automatically, and the app waits out the server's full retry budget instead of re-sending. Users see macros, not model outages.
F-03Context-aware AI assistant
A Groq-powered chat tab that's fed today's log, remaining macros and 7-day averages. Multi-turn, with server-side safety guardrails.
F-04Meal templates
Multi-select foods from the library into a named template, then log an entire breakfast in one tap instead of four.
F-05Real push notifications
VAPID web push with per-user preferences: goal alerts, meal reminders and a weekly summary, each at a time the user picks.
F-06User-controlled PWA updates
New versions don't force-reload mid-log: a banner shows exactly what changed (from a versioned changelog) and the user applies it when ready.
F-07Trends without a chart library
The 7-day trends tab renders its charts as raw SVG: no charting dependency, full control over the visuals, smaller bundle.
F-08Cost-capped by design
A 10-scans-per-day limit per account, plus per-user and per-IP burst caps checked before the paid call, keeps the vision API bill bounded while staying generous for real daily use.
F-09Abuse guards that block, not just alert
One middleware: 300+ requests a minute or a 30-URL 404 walk from one IP earns a 10-minute 429, keyed on the Cloudflare-verified client IP. CORS stays outermost so blocked responses remain readable to the browser.
F-10Recycle bin inside Postgres
SECURITY DEFINER BEFORE DELETE triggers on nine tables copy every deleted row as JSONB into a table the app role cannot read or purge. Retention is hard-coded at 30 days; explicit account deletion bypasses it via a transaction-local GUC.
F-11Two-layer account freeze
60 verified deletes in ten minutes from one session: every route returns 423 instantly from an in-process lock, and a Supabase admin ban (issued off the event loop) makes it stick across restarts. Reversal is a deliberate runbook step.
F-12Two-channel observability
ntfy.sh for ~25 hand-written ops events (quota, spikes, keepalive, blocks, freezes) with per-event cooldowns and rolling-window spike detection; Sentry for unhandled exceptions with stack trace and request context.
F-13Uploads that cannot lie
One server-side gate for every image: magic-byte format sniffing (the client's content-type is never trusted), polyglot rejection, a decompression-bomb cap, verify-then-decode, and a metadata-free re-encode. An HTML file named .jpg, a PNG declaring a 30000x30000 canvas, and a photo with "ignore the label" printed on it each have a test; the model's JSON is schema-checked and rejected, never clamped.
Scan to open NutriScan
Point your phone camera at the code to launch the live app instantly: no install needed, it runs as a PWA you can add to your home screen. Every account gets 10 free scans a day.
nutritional-tracker-delta.vercel.app github.com/MrTig-afk/NutritionalTracker